
註冊完帳號後,本篇要來更新資料和上傳大頭照。
首先先新增 Controller:
php artisan make:controller User/UserController
再來開2個路由,顯示頁面和更新用戶:
routes/web.php
// User
Route::get('user/setting', 'User\UserController@edit');
Route::put('user', 'User\UserController@update');
為了方便在 Controller 裡呼叫 已登入用戶,在主要的 Controller 裡增加一個 user():
app/Http/Controllers/Controller.php
use App\User;
use Illuminate\Support\Facades\Auth;
protected function user(): ?User
{
    return Auth::user();
}
然後要輸出編輯用戶的頁面。還有在 UserController 裡都是已登入用戶才能操作,因此要在 __construct() 裡加一個 auth Middleware:
app/Http/Controllers/User/UserController.php
use App\Presenters\UserPresenter;
public function __construct()
{
    $this->middleware('auth');
}
public function edit()
{
    return Inertia::render('User/Edit', [
        'user' => UserPresenter::make($this->user())->get(),
    ]);
}
還有頁面:
resources/js/Pages/User/Edit.vue
<template>
  <div class="py-6 md:py-8">
    <form @submit.prevent="submit" class="card card-main">
      <h1 class="text-3xl font-semibold text-center">帳號設定</h1>
      <div class="w-12 mt-1 mx-auto border-b-4 border-purple-400"></div>
      <div class="grid gap-6 mt-6 md:grid-cols-2">
        <text-input v-model="form.name" :error="$page.errors.name" label="姓名" autocomplete="name" />
        <text-input v-model="form.email" :error="$page.errors.email" label="E-mail" autocomplete="email" />
        <textarea-input v-model="form.description" :error="$page.errors.description" class="md:col-span-2" label="個人簡介" />
        <text-input v-model="form.password" :error="$page.errors.password" type="password" label="密碼" />
        <text-input v-model="form.password_confirmation" type="password" label="確認密碼" />
        <file-input v-model="form.avatar" :error="$page.errors.avatar" accept="image/*" label="大頭照" browseText="選擇圖片" />
        <div class="md:col-span-2">
          <loading-button :loading="loading" class="btn btn-purple">更新帳號</loading-button>
        </div>
      </div>
    </form>
  </div>
</template>
<script>
import AppLayout from '@/Layouts/AppLayout'
import TextInput from '@/Components/TextInput'
import TextareaInput from '@/Components/TextareaInput'
import FileInput from '@/Components/FileInput'
import LoadingButton from '@/Components/LoadingButton'
export default {
  layout: AppLayout,
  metaInfo: {
    title: '帳號設定'
  },
  components: {
    TextInput,
    TextareaInput,
    FileInput,
    LoadingButton
  },
  props: {
    user: Object
  },
  data() {
    return {
      form: {
        name: this.user.name,
        email: this.user.email,
        description: this.user.description,
        password: '',
        password_confirmation: '',
        avatar: null
      },
      loading: false
    }
  },
  methods: {
    submit() {
      const data = new FormData()
      for (const key in this.form) {
        data.append(key, this.form[key] || '')
      }
      data.append('_method', 'put')
      this.$inertia.post('/user', data, {
        onStart: () => this.loading = true,
        onFinish: () => this.loading = false,
        onSuccess: () => {
          if (! Object.keys(this.$page.errors).length) {
            this.form.password = ''
            this.form.password_confirmation = ''
            this.form.avatar = null
          }
        }
      })
    }
  }
}
</script>
Inertia.js v0.3 已棄用 Promise 調用方式
現在全系列已更新為 Inertia.js v0.3,增加了 Event system (事件系統),Promise 調用的方式已棄用,若尚未更新至 v0.3 請更新版本:
yarn add @inertiajs/inertia@^0.3 @inertiajs/inertia-vue@^0.2.4並參考 Day 09 Lightning 用戶登入 的「載入進度條」篇安裝進度條套件。
但如果你還是想要使用舊方法或者不想升級,請參考以下用法:
submit() { this.loading = true const data = new FormData() for (const key in this.form) { data.append(key, this.form[key] || '') } data.append('_method', 'put') this.$inertia.post('/user', data).then(() => { this.loading = false if (! Object.keys(this.$page.errors).length) { this.form.password = '' this.form.password_confirmation = '' this.form.avatar = null } }) }
這次多了 TextareaInput 和 FileInput,為了不佔版面,可以直接去我的 Lightning GitHub 倉庫的 Components 裡拿。
再來要注意的是,雖然 Inertia 可以直接呼叫 this.$inertia.put(),但這裡有用 FormData 傳大頭照過去,不能用 put 方法傳送。只能跟 Laravel 一樣,要用 post 加 _method=put。
還有這次用到的 CSS。@screen 是設定斷點,算是 @media ... 的 Tailwind CSS 縮寫:
resources/css/components.css
/* Card */
...
.card-main {
  @apply max-w-screen-md p-6 mx-auto;
  @screen md {
    @apply p-8;
  }
}
resources/css/button.css
/* Card */
...
.btn-purple-light {
  @apply bg-purple-100 text-purple-700;
  &:hover {
    @apply bg-purple-200;
  }
}
.btn-red {
  @apply bg-red-500 text-white;
  &:hover {
    @apply bg-red-700;
  }
  &:disabled {
    @apply bg-red-300 !important;
  }
}
.btn-red-light {
  @apply bg-red-100 text-red-700;
  &:hover {
    @apply bg-red-200;
  }
}
然後瀏覽 /user/setting:

最後在右上選單裡增加連結:
resources/js/Layouts/AppLayout.vue
<template #menu="{ close }">
  <dropdown-item href="/user/setting" icon="heroicons-outline:cog" @click="close">
    帳號設定
  </dropdown-item>
  <hr class="border-gray-200 my-2">
  ...
</template>

表單頁面好了之後,再來是處裡後端表單驗證和儲存資料的部分。新增一個 UpdateUserRequest,把驗證表單的規則都寫在裡面:
php artisan make:request UpdateUserRequest
預設的 authorize() 不會用到,直接刪掉沒關係。然後在 rules() 裡寫驗證規則:
app/Http/Requests/UpdateUserRequest.php
use Illuminate\Validation\Rule;
public function rules()
{
    return [
        'name' => 'required|string|max:255',
        'email' => [
            'required', 'string', 'email', 'max:255',
            Rule::unique('users')->ignore($this->user()->id),
        ],
        'description' => 'nullable|string|max:100',
        'password' => 'nullable|string|min:8|confirmed',
        'avatar' => 'nullable|image|max:5120',
    ];
}
password 和 avatar 欄位都是希望有輸入(或上傳檔案)才更新,沒有輸入也要可以通過驗證,這時可以使用 nullable 規則,允許 null 或不存在的值。但如果直接設 null 進資料庫會有問題,還要在 UpdateUserRequest 裡增加 validationData(),調整需要驗證的資料:
app/Http/Requests/UpdateUserRequest.php
public function validationData()
{
    $data = parent::validationData();
    if (! $data['password']) {
        unset($data['password']);
    }
    if (! $data['avatar']) {
        unset($data['avatar']);
    }
    return $data;
}
然後把 Hash 密碼和儲存上傳圖片的動作放在 User Model 裡:
app/User.php
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage;
public function setPasswordAttribute($value)
{
    $this->attributes['password'] = Hash::needsRehash($value) ? Hash::make($value) : $value;
}
public function setAvatarAttribute($avatar)
{
    $this->attributes['avatar'] = $avatar instanceof UploadedFile
        ? Storage::url($avatar->store('avatars'))
        : $avatar;
}
回到 UserController 更新用戶資料。除了可以回傳 Inertia 響應 也可以重新導向。更新後還要返回表單頁 (上一頁),只要使用 back() 即可:
app/Http/Controllers/User/UserController.php
use App\Http\Requests\UpdateUserRequest;
public function update(UpdateUserRequest $request)
{
    $this->user()->update($request->validated());
    return back();
}
現在可以去修改看看用戶資料囉!但更新後沒有任何回應訊息,有點沒安全感。這裡可以用 Flash Session 功能 (只會出現一次的 Session),在 redirect 物件後串上 with():
app/Http/Controllers/User/UserController.php
public function update(UpdateUserRequest $request)
{
    ...
    return back()->with('success', '帳號更新成功');
}
然後新增成功/錯誤訊息的共享資料:
app/Providers/AppServiceProvider.php
protected function registerInertia()
{
    Inertia::share([
        ...
        'flash' => fn () => [
            'success' => session('success'),
            'error' => session('error'),
        ],
    ]);
}
新增一個 Alert 組件,這就是提示訊息用的組件:
resources/js/Components/Alert.vue
<template>
  <div v-if="type === 'success'" class="alert alert-success">
    <icon class="mr-1" icon="heroicons-outline:check-circle" />
    <slot />
  </div>
  <div v-else-if="type === 'error'" class="alert alert-error">
    <icon class="mr-1" icon="heroicons-outline:x-circle" />
    <slot />
  </div>
</template>
<script>
export default {
  props: {
    type: {
      type: String,
      default: 'success'
    }
  }
}
</script>
resources/css/components.css
/* Alert */
.alert {
  @apply flex items-center px-5 py-3 text-lg rounded-md;
}
.alert-success {
  @apply bg-green-100 text-green-700;
}
.alert-error {
  @apply bg-red-100 text-red-700;
}
引入 Alert 組件,成功訊息可以在 $page.flash.success 讀取:
resources/js/Pages/User/Edit.vue
<template>
  <div class="py-6 md:py-8">
    <form @submit.prevent="submit" class="card card-main">
      <h1 class="text-3xl font-semibold text-center">帳號設定</h1>
      <div class="w-12 mt-1 mx-auto border-b-4 border-purple-400"></div>
      <alert v-if="$page.flash.success" class="mt-6">{{ $page.flash.success }}</alert>
      ...
    </form>
    ...
  </div>
</template>
<script>
import Alert from '@/Components/Alert'
export default {
  components: {
    Alert,
    ...
  }
}
</script>

雖然我本來想要加上刪除帳號的篇章,但無奈...實在是太長了,只能移到下次。下篇是刪除帳號和用戶頁面,用戶功能的最後一篇。
Lightning 範例程式碼:https://github.com/ycs77/lightning